package com.mo.tools.serialize;

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.std.DateDeserializers;
import com.fasterxml.jackson.databind.ser.std.DateSerializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.*;

/**
 * @author: e-Xingfu.Yang
 * @description:
 * @date:created in 2021/7/9 9:59
 * @modificed by:
 */
@SuppressWarnings("unchecked")
public class JacksonBundle {


    private static Logger log = LoggerFactory.getLogger(JacksonBundle.class);

    private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
    private static final String DEFAULT_DATE = "yyyy-MM-dd";
    private static final String DEFAULT_TIME = "HH:mm:ss";
    private PropertyNamingStrategy propertyNamingStrategy = PropertyNamingStrategy.LOWER_CAMEL_CASE;

    private ObjectMapper mapper;

    protected Map<Class<?>, ModelStore> serializationMap = new LinkedHashMap<>(4);
    protected Map<Class<?>, ModelStore> deserializationMap = new LinkedHashMap<>(4);

    protected Include include;
    protected boolean defaultInstance;

    public JacksonBundle() {
        this(null);
    }

    public JacksonBundle(Include include) {
        mapper = new ObjectMapper();

        defaultInstance = true;
        if (include != null) {
            mapper.setSerializationInclusion(include);
            this.include = include;
        }
        mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        mapper.setPropertyNamingStrategy(propertyNamingStrategy);
        setSerializerAndDeSerializerDateFormat(null);
    }

    /**
     * include object all attributes
     *
     * @return
     */
    public static JacksonBundle defaultMapper() {
        return new JacksonBundle(Include.ALWAYS);
    }

    /**
     * exclude null-valued attributes
     *
     * @return
     */
    public static JacksonBundle nonNullMapper() {
        return new JacksonBundle(Include.NON_NULL);
    }

    /**
     * include Include.NON_NULL strategy, exclude empty-valued attributes
     * exclude with [null, "", empty collection, empty array...]
     *
     * @return
     */
    public static JacksonBundle nonEmptyMapper() {
        return new JacksonBundle(Include.NON_EMPTY);
    }

    /**
     * include Include.NON_EMPTY strategy, exclude empty-valued and object-default-value attributes
     * exclude with[0, 0.0, null, null, empty collection, empty array...]
     *
     * @return
     */
    public static JacksonBundle nonDefaultMapper() {
        return new JacksonBundle(Include.NON_DEFAULT);
    }

    /**
     * include object all attributes
     *
     * @return
     */
    public static JacksonBundle build() {
        return new JacksonBundle();
    }


    /**
     * object convert to json
     *
     * @param o
     * @return
     */
    public String toJson(Object o) {
        if (Objects.isNull(o)) {
            return null;
        }
        try {
            return mapper.writeValueAsString(o);
        } catch (JsonProcessingException e) {
            log.error("object convert to json error", e);
            return null;
        }
    }

    /**
     * object convert to pretty json
     *
     * @param obj
     * @return
     */
    public String toPrettyJson(Object obj) {
        try {
            return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            log.error("object convert to pretty json error", e);
        }
        return null;
    }

    /**
     * json convert to Object, exclude Collection type
     *
     * @param json
     * @param clazz
     * @param <T>
     * @return
     */
    public <T> T toObject(String json, Class<T> clazz) {
        if (Objects.isNull(json)) {
            return null;
        }
        try {
            return mapper.readValue(json, clazz);
        } catch (Exception e) {
            log.error("json convert to object error", e);
        }
        return null;
    }

    /**
     * json convert to ArrayList, custom collection type, use 'toCollection' method
     *
     * @param json
     * @param clazz
     * @param <T>
     * @return
     */
    public <T> List<T> toList(String json, Class<T> clazz) {
        if (ObjectUtils.isEmpty(json)) {
            return null;
        }

        if (ObjectUtils.isEmpty(clazz)) {
            log.warn("clazz type is null, can not parse json");
            return null;
        }

        return toCollection(json, ArrayList.class, clazz);
    }

    /**
     * json convert to LinkedHashSet, custom collection type, use 'toCollection' method
     *
     * @param json
     * @param clazz
     * @param <T>
     * @return
     */
    public <T> Set<T> toSet(String json, Class<T> clazz) {
        if (ObjectUtils.isEmpty(json)) {
            return null;
        }

        if (ObjectUtils.isEmpty(clazz)) {
            log.warn("clazz type is null, can not parse json");
            return null;
        }

        return toCollection(json, LinkedHashSet.class, clazz);
    }

    /**
     * json to Map<String, String>
     * custom complex type, use 'toCollection' method
     *
     * @param json
     * @return
     */
    public Map<String, String> toMap(String json) {
        if (ObjectUtils.isEmpty(json)) {
            return null;
        }

        try {
            return mapper.readValue(json, new TypeReference<LinkedHashMap<String, String>>() {
            });
        } catch (IOException e) {
            log.error("json convert to LinkedHashMap<String, String> error", e);
        }
        return null;
    }


    /**
     * custom conversion types and complex type handling
     * ArrayList, eg: toCollection(json, ArrayList.class, Bean.class);
     * HashMap<String, String>, eg: toCollection(json, HashMap.class, String.class, String.class);
     *
     * @param json
     * @param collectionClass
     * @param elementClass
     * @param <T>
     * @param <C>
     * @return
     */
    public <T, C> C toCollection(String json, Class<C> collectionClass, Class<T>... elementClass) {
        if (ObjectUtils.isEmpty(json)) {
            return null;
        }

        if (ObjectUtils.isEmpty(collectionClass)) {
            log.warn("collection type is null, can not parse json");
            return null;
        }

        if (ObjectUtils.isEmpty(elementClass)) {
            log.warn("element type is null, can not parse json");
            return null;
        }

        JavaType javaType = mapper.getTypeFactory().constructParametricType(collectionClass, elementClass);
        try {
            return mapper.readValue(json, javaType);
        } catch (IOException e) {
            log.error("json convert to collection error", e);
        }
        return null;
    }

    /**
     * custom conversion types and complex type handling
     * ArrayList, eg: toCollection(json, new TypeReference<ArrayList<Bean>>() {});
     * HashMap<String, String>, eg: toCollection(json, new TypeReference<HashMap<String, String>>() {});
     *
     * @param json
     * @param typeReference
     * @param <T>
     * @return
     */
    public <T> T toCollection(String json, TypeReference<T> typeReference) {
        if (ObjectUtils.isEmpty(json)) {
            return null;
        }
        if (ObjectUtils.isEmpty(typeReference)) {
            log.warn("collection type is null, can not parse json");
            return null;
        }
        try {
            return mapper.readValue(json, typeReference);
        } catch (IOException e) {
            log.error("json convert to LinkedHashMap<String, String> error", e);
        }
        return null;
    }

    /**
     * specifies the Date, LocalDate, LocalTime, LocalDateTime serialization and deserialization types
     *
     * @param pattern eg: ("yyyy-mm-dd HH:mm:ss")
     * @return
     */
    public JacksonBundle setSerializerAndDeSerializerDateFormat(String pattern) {

        // set Date LocalDate  LocalTime LocalDateTime format
        serializationMap.put(LocalDateSerializer.class, new ModelStore(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE))));
        serializationMap.put(LocalTimeSerializer.class, new ModelStore(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME))));
        deserializationMap.put(LocalDateDeserializer.class, new ModelStore(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE))));
        deserializationMap.put(LocalTimeDeserializer.class, new ModelStore(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME))));
        if (ObjectUtils.isEmpty(pattern)) {
            serializationMap.put(DateSerializer.class, new ModelStore(Date.class, new DateSerializer(false, new SimpleDateFormat(DEFAULT_DATE_FORMAT))));
            serializationMap.put(LocalDateTimeSerializer.class, new ModelStore(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))));
            deserializationMap.put(DateDeserializers.DateDeserializer.class, new ModelStore(Date.class, new DateDeserializers.DateDeserializer(
                    DateDeserializers.DateDeserializer.instance, new SimpleDateFormat(DEFAULT_DATE_FORMAT), null)));
            deserializationMap.put(LocalDateTimeDeserializer.class, new ModelStore(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))));
        } else {
            serializationMap.put(DateSerializer.class, new ModelStore(Date.class, new DateSerializer(false, new SimpleDateFormat(pattern))));
            serializationMap.put(LocalDateTimeSerializer.class, new ModelStore(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(pattern))));
            deserializationMap.put(DateDeserializers.DateDeserializer.class, new ModelStore(Date.class, new DateDeserializers.DateDeserializer(
                    DateDeserializers.DateDeserializer.instance, new SimpleDateFormat(pattern), null)));
            deserializationMap.put(LocalDateTimeDeserializer.class, new ModelStore(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(pattern))));
        }
        setJavaTimeModules();
        return this;
    }

    /**
     * specifies the Date, LocalDate, LocalTime, LocalDateTime serialization type
     *
     * @param pattern eg: ("yyyy-mm-dd HH:mm:ss")
     * @return
     */
    public JacksonBundle setSerializerDateFormat(String pattern) {
        // set Date LocalDate and LocalTime format
        serializationMap.put(LocalDateSerializer.class, new ModelStore(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE))));
        serializationMap.put(LocalTimeSerializer.class, new ModelStore(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME))));

        // set Date and LocalDateTime format
        if (ObjectUtils.isEmpty(pattern)) {
            serializationMap.put(DateSerializer.class, new ModelStore(Date.class, new DateSerializer(false, new SimpleDateFormat(DEFAULT_DATE_FORMAT))));
            serializationMap.put(LocalDateTimeSerializer.class, new ModelStore(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))));
        } else {
            serializationMap.put(DateSerializer.class, new ModelStore(Date.class, new DateSerializer(false, new SimpleDateFormat(pattern))));
            serializationMap.put(LocalDateTimeSerializer.class, new ModelStore(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(pattern))));

        }
        setJavaTimeModules();
        return this;
    }

    /**
     * specifies the Date, LocalDate, LocalTime, LocalDateTime deserialization type
     *
     * @param pattern eg: ("yyyy-mm-dd HH:mm:ss")
     * @return
     */
    public JacksonBundle setDeSerializerDateFormat(String pattern) {
        // set  LocalDate and LocalTime format
        deserializationMap.put(LocalDateDeserializer.class, new ModelStore(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE))));
        deserializationMap.put(LocalTimeDeserializer.class, new ModelStore(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME))));

        // set Date and LocalDateTime format
        if (ObjectUtils.isEmpty(pattern)) {
            deserializationMap.put(DateDeserializers.DateDeserializer.class, new ModelStore(Date.class, new DateDeserializers.DateDeserializer(
                    DateDeserializers.DateDeserializer.instance, new SimpleDateFormat(DEFAULT_DATE_FORMAT), null)));
            deserializationMap.put(LocalDateTimeDeserializer.class, new ModelStore(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))));

        } else {
            deserializationMap.put(DateDeserializers.DateDeserializer.class, new ModelStore(Date.class, new DateDeserializers.DateDeserializer(
                    DateDeserializers.DateDeserializer.instance, new SimpleDateFormat(pattern), null)));
            deserializationMap.put(LocalDateTimeDeserializer.class, new ModelStore(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(pattern))));
        }
        setJavaTimeModules();
        return this;
    }

    /**
     * set and update the date module
     *
     * @return
     */
    private JacksonBundle setJavaTimeModules() {
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        if (!CollectionUtils.isEmpty(serializationMap)) {
            for (ModelStore modelStore : serializationMap.values()) {
                javaTimeModule.addSerializer(modelStore.getClazz(), modelStore.getJsonSerializer());
            }
        }
        if (!CollectionUtils.isEmpty(deserializationMap)) {
            for (ModelStore modelStore : deserializationMap.values()) {
                javaTimeModule.addDeserializer(modelStore.getClazz(), modelStore.getJsonDeserializer());
            }
        }

        mapper = defaultInstance ? this.mapper : new ObjectMapper();
        if (!CollectionUtils.isEmpty(serializationMap) || !CollectionUtils.isEmpty(deserializationMap)) {
            mapper = new ObjectMapper();
            if (include != null) {
                mapper.setSerializationInclusion(include);
            }
            mapper.setPropertyNamingStrategy(propertyNamingStrategy);
            mapper.registerModule(javaTimeModule);
            mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        }
        return this;
    }

    /**
     * json deserialize to object and append to appoint object
     * @param json
     * @param object
     * @param <T>
     * @return
     */
    public <T> T appendObject(String json, T object) {
        try {
            return (T) mapper.readerForUpdating(object).readValue(json);
        } catch (IOException e) {
            log.error("json deserialize to object and append to appoint object exception, e");
        }
        return null;
    }

    /**
     * set field serialize format
     * KebabCase: eg: object-field
     * SnakeCase: eg: object_field
     * LowerCase: eg: objectField
     * UpperCamelCase: eg: ObjectField
     * @param propertyNamingStrategy
     */
    public void setPropertyNamingStrategy(PropertyNamingStrategy propertyNamingStrategy) {
        this.propertyNamingStrategy = propertyNamingStrategy;
        setJavaTimeModules();
    }

    public ObjectMapper getMapper() {
        return mapper;
    }

    public void setMapper(ObjectMapper mapper) {
        this.mapper = mapper;
    }


    static class ModelStore {
        private Class clazz;
        private JsonSerializer<?> jsonSerializer;
        private JsonDeserializer<?> jsonDeserializer;

        public ModelStore(Class clazz, JsonSerializer<?> jsonSerializer) {
            this.clazz = clazz;
            this.jsonSerializer = jsonSerializer;
        }

        public ModelStore(Class clazz, JsonDeserializer<?> jsonDeserializer) {
            this.clazz = clazz;
            this.jsonDeserializer = jsonDeserializer;
        }

        public Class getClazz() {
            return clazz;
        }

        public JsonSerializer<?> getJsonSerializer() {
            return jsonSerializer;
        }

        public JsonDeserializer<?> getJsonDeserializer() {
            return jsonDeserializer;
        }
    }

}
